using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using static System.Console;

enum ExitCode
{
    Chaos = -44,
    Singleton = -2,
    Exception = -1,
    Success,
    Syntax,
    DirNotExist,
    DirUnsafe,
}

class Program
{
    static void ErrorExit(ExitCode code)
    {
        WriteLine("Exiting in 3 seconds...");
        Thread.Sleep(3000);
        Environment.Exit((int)code);
    }

    static void CheckDir(string targetDir)
    {
        if (!Directory.Exists(targetDir))
        {
            WriteLine("Target directory does not exist!");
            ErrorExit(ExitCode.DirNotExist);
        }

        if (new DirectoryInfo(targetDir).Parent == null)
        {
            WriteLine("Cannot run on root directory!");
            ErrorExit(ExitCode.DirUnsafe);
        }

        var unsafeDirs = new []
        {
            Path.GetFullPath(Environment.SystemDirectory + @"\.."),
            Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles),
            Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86),
            Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData),
            Path.GetFullPath(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) + @"\..")
        };

        foreach (var unsafeDir in unsafeDirs)
        {
            if (IsSubPath(targetDir, unsafeDir))
            {
                WriteLine("Cannot run on system directory: {0}", unsafeDir);
                ErrorExit(ExitCode.DirUnsafe);
            }
        }
    }

    static bool IsSubPath(string path, string parentPath)
    {
        int lastIndex = path.Length;
        do
        {
            if (string.Compare(parentPath, 0, path, 0, lastIndex, true) == 0) return true;
            lastIndex = path.LastIndexOf(Path.DirectorySeparatorChar, lastIndex - 1);
            if (lastIndex < 0) lastIndex = path.LastIndexOf(Path.AltDirectorySeparatorChar);
        } while (lastIndex >= 0);
        return false;
    }

    static void Main(string[] args)
    {
        var processor = new Processor();

        //
        // Parse commandline arguments
        //
        for (int i = 0; i < args.Length; i++)
        {
            // Inner function to parse arg list.
            void parseArgList(List<string> list)
            {
                for (i++; i < args.Length; i++)
                {
                    if (IsArg(args[i]))
                    {
                        i--;
                        break;
                    }
                    
                    list.Add(args[i]);
                }
            }

            string getNextArg()
            {
                i++;
                if (i < args.Length) return args[i];
                return null;
            }

            var arg = args[i];

            if (IsArg(arg))
            {
                var argName = arg.Substring(1);

                if (argName == "xd")
                {
                    parseArgList(processor.ExcludeDirNames);
                }
                else if (argName == "xdr")
                {
                    parseArgList(processor.ExcludeDirPatterns);
                }
                else if (argName == "xf")
                {
                    parseArgList(processor.ExcludeFileNames);
                }
                else if (argName == "xfr")
                {
                    parseArgList(processor.ExcludeFilePatterns);
                }
                else if (argName == "age")
                {
                    arg = getNextArg();
                    if (arg == null)
                    {
                        WriteLine($"No age value specified, using default {processor.FileAgeToDelete}.");
                    }
                    else
                    {
                        if (int.TryParse(arg, out var seconds) && seconds >= 0)
                        {
                            processor.FileAgeToDelete = TimeSpan.FromSeconds(seconds);
                        }
                        else
                        {
                            WriteLine($"Invalid file age: {arg}.");
                            Environment.Exit(1);
                        }
                    }
                }
                else if (argName == "nap")
                {
                    arg = getNextArg();
                    if (arg == null)
                    {
                        WriteLine($"No nap value specified, using default {processor.NapMilliseconds / 1000.0}.");
                    }
                    else
                    {
                        if (int.TryParse(arg, out var seconds) && seconds >= 0)
                        {
                            processor.NapMilliseconds = seconds * 1000;
                        }
                        else
                        {
                            WriteLine($"Invalid nap duration: {arg}.");
                            Environment.Exit(1);
                        }
                    }
                }
                else if (argName == "h" || argName == "?" || argName == "help")
                {
                    WriteLine("XTMP [/xd[x] name...]... [/xf[x] name...]... [/age seconds] [/nap seconds]");
                    WriteLine("The optional x modifier means following filenames are Regex patterns.");
                    Environment.Exit(0);
                }
                else
                {
                    WriteLine($"Invalid option: {argName}.");
                    Environment.Exit(1);
                }
            }
            else
            {
                if (processor.TargetDir == null) processor.TargetDir = arg;
                else
                {
                    WriteLine("Superfluous argument on commandline.");
                    Environment.Exit(1);
                }
            }
        }

        //
        // Use hash string of target directory as the name of mutex to ensure
        // only one single instance of this program running for a specific directory.
        //
        var md5 = MD5.Create();
        var targetDir = processor.GetNormalizedTargetDir();
        // Remove trailing backslash if any
        if (Path.EndsInDirectorySeparator(targetDir)) targetDir = targetDir.Substring(0, targetDir.Length - 1);
        var hash = md5.ComputeHash(Encoding.Default.GetBytes(targetDir.ToLower()));
        var mutexName = Convert.ToBase64String(hash);
        var mutex = new Mutex(true, mutexName, out var createdNew);
        if (!createdNew)
        {
            WriteLine("There is already an instance running on target directory: {0}", targetDir);
            ErrorExit(ExitCode.Singleton);
        }
        else
        {
            try
            {
                if (processor.TargetDir != null) CheckDir(targetDir);
                processor.Run();
            }
            catch(Exception ex)
            {
                WriteLine(ex);
                ErrorExit(ExitCode.Exception);
            }
        }
    }

    static bool IsArg(string arg) => arg.StartsWith('-') || arg.StartsWith('/');
}

class Processor
{
    public TimeSpan FileAgeToDelete { get; set; } = TimeSpan.FromMinutes(5);
    public int NapMilliseconds { get; set; } = 30000;

    public string TargetDir { get; set; }
    public List<string> ExcludeDirNames { get; set; } = new List<string>();
    public List<string> ExcludeDirPatterns { get; set; } = new List<string>();
    public List<string> ExcludeFileNames { get; set; } = new List<string>();
    public List<string> ExcludeFilePatterns { get; set; } = new List<string>();

    public string GetNormalizedTargetDir() => Path.GetFullPath(this.TargetDir ?? Path.GetTempPath());

    public void Run()
    {
        var targetDir = GetNormalizedTargetDir();
        do {
            Trace("***Batch started");
            Trace($"Target directory is {targetDir}");
            DeleteTmpFiles(targetDir);
            Trace($"***Batch finished");
            Trace($"***Sleeping for {this.NapMilliseconds / 1000.0} seconds...");
            Thread.Sleep(this.NapMilliseconds);
        } while (true);
    }

    void DeleteTmpFiles(string targetDir)
    {
        foreach (var filepath in Directory.GetFiles(targetDir, "*.*"))
        {
            var fileName = Path.GetFileName(filepath);

            if (IsFileNameInList(fileName, this.ExcludeFileNames)) continue;
            if (IsFileNameInPatternList(fileName, this.ExcludeFilePatterns)) continue;
            
            var wt = File.GetLastWriteTime(filepath);
            if (DateTime.Now - wt > FileAgeToDelete)
            {
                try
                {
                    File.Delete(filepath);
                    Trace($"[XF] {fileName}...");
                }
                catch (Exception)
                {
                    Trace($"[!F] {fileName}");
                }
            }
        }

        foreach (var filepath in Directory.GetDirectories(targetDir))
        {
            var dirName = Path.GetFileName(filepath);

            if (IsFileNameInList(dirName, this.ExcludeDirNames)) continue;
            if (IsFileNameInPatternList(dirName, this.ExcludeDirPatterns)) continue;
            
            var wt = Directory.GetLastWriteTime(filepath);
            if (DateTime.Now - wt > FileAgeToDelete)
            {
                try
                {
                    Directory.Delete(filepath, true);
                    Trace($"[XD] {dirName}...");
                }
                catch (Exception)
                {
                    Trace($"[!D] {dirName}");
                }
            }
        }

        bool IsFileNameInList(string filename, List<string> list)
            => list.Any(pattern_ => string.Compare(filename, pattern_, true) == 0);

        bool IsFileNameInPatternList(string filename, List<string> list)
            => list.Any(pattern_ => Regex.IsMatch(filename, pattern_));
    }

    public static void Trace(string text) => WriteLine($"[{DateTime.Now}] {text}");
}